﻿using System;
using System.Collections.Generic;
//using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using thesebas.socks5proxy;
using System.IO;

namespace thesebas.socks5proxy
{
    public class Socks5proxyException : Exception
    {
        public Socks5proxyException() : base() { }
        public Socks5proxyException(string message) : base(message) { }
        public Socks5proxyException(string message, Exception innerException) : base(message, innerException) { }
    }

    public class Debug
    {
        public static void PrintPacket(byte[] packet, string label)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("packet [{0}]\n", label);
            foreach (byte b in packet)
            {
                sb.AppendFormat("{0:X} ", b);
            }

            Console.WriteLine(sb.ToString());
        }
    }

    public class Proxy
    {

        TcpClient tcpClient;
        private string username;
        private string password;
        private Proxy.AuthType authType = AuthType.NoAuth;
        public enum AuthType { NoAuth = 0, UsernamePassword = 2 }

        private IPEndPoint proxyEndpoint;

        public Proxy(IPAddress address, int port) : this(new IPEndPoint(address, port)) { }

        public Proxy(IPEndPoint proxy)
        {
            this.proxyEndpoint = proxy;
        }
        //public Proxy SetAuthType(Proxy.AuthType type)
        //{
        //    this.authType = type;
        //    return this;
        //}
        //public Proxy SetAuth(string username, string password)
        //{
        //    this.username = username;
        //    this.password = password;
        //    return this;
        //}
       
        public TcpClient ConnectTcpClient(IPAddress address, int port)
        {
            return this.ConnectTcpClient(new IPEndPoint(address, port));
        }

        public TcpClient ConnectTcpClient(string hostname, int port, bool useRemoteDns) {
            IPAddress addr;
            if(IPAddress.TryParse(hostname, out addr)){
                return this.ConnectTcpClient(addr, port);
            }
            if (!useRemoteDns) {
                IPHostEntry entry = Dns.GetHostEntry(hostname);
                if (entry.AddressList.Length == 0) throw new Socks5proxyException(String.Format("unresolved hostaname [{0}]", hostname));
                return this.ConnectTcpClient(entry.AddressList[0], port);
            }
            throw new Socks5proxyException("remote dns not supported, yet");
        
        }
        public TcpClient ConnectTcpClient(IPEndPoint destination)
        {
            this.tcpClient = new TcpClient();
            this.tcpClient.Connect(this.proxyEndpoint);
            //StreamWriter swrt=new StreamWriter(this.tcpClient.GetStream());
            //StreamReader srdr=new StreamReader(this.tcpClient.GetStream());
            // 
            BinaryWriter wrt = new BinaryWriter(this.tcpClient.GetStream());

            BinaryReader rdr = new BinaryReader(this.tcpClient.GetStream());
            byte[] req, resp;
            //o  X'00' NO AUTHENTICATION REQUIRED
            //o  X'01' GSSAPI
            //o  X'02' USERNAME/PASSWORD
            //o  X'03' to X'7F' IANA ASSIGNED
            //o  X'80' to X'FE' RESERVED FOR PRIVATE METHODS
            //o  X'FF' NO ACCEPTABLE METHODS

            //+----+----------+----------+
            //|VER | NMETHODS | METHODS  |
            //+----+----------+----------+
            //| 1  |    1     | 1 to 255 |
            //+----+----------+----------+

            req = new byte[] { 5, 1, 0 };
            //Debug.PrintPacket(req, "hello request");
            wrt.Write(req);


            //+----+--------+
            //|VER | METHOD |
            //+----+--------+
            //| 1  |   1    |
            //+----+--------+
            resp = new byte[2];
            resp = rdr.ReadBytes(2);
            //Debug.PrintPacket(req, "hello response");
            if (resp[0] != 5) throw new Socks5proxyException(string.Format("proxy verision reponse != 5 [{0:X}]", resp[0]));
            if (resp[1] != 0) throw new Socks5proxyException(string.Format("proxy auth type not supported [{0:X}]", resp[1]));

            //The SOCKS request is formed as follows:

            //     +----+-----+-------+------+----------+----------+
            //     |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
            //     +----+-----+-------+------+----------+----------+
            //     | 1  |  1  | X'00' |  1   | Variable |    2     |
            //     +----+-----+-------+------+----------+----------+

            //  Where:

            //       o  VER    protocol version: X'05'
            //       o  CMD
            //          o  CONNECT X'01'
            //          o  BIND X'02'
            //          o  UDP ASSOCIATE X'03'
            //       o  RSV    RESERVED
            //       o  ATYP   address type of following address
            //          o  IP V4 address: X'01'
            //          o  DOMAINNAME: X'03'
            //          o  IP V6 address: X'04'
            //       o  DST.ADDR       desired destination address
            //       o  DST.PORT desired destination port in network octet
            //          order

            //  ###ATYP field
            //      o  X'01'

            //the address is a version-4 IP address, with a length of 4 octets

            //       o  X'03'

            //the address field contains a fully-qualified domain name.  The first
            //octet of the address field contains the number of octets of name that
            //follow, there is no terminating NUL octet.

            //       o  X'04'

            //the address is a version-6 IP address, with a length of 16 octets.

            req = new byte[] { 5, 1, 0, 1, /*addr IPv4*/0, 0, 0, 0,/*port*/0, 0 };
            byte[] _port = new byte[] { (byte)(destination.Port >> 8 & 0xff), (byte)(destination.Port & 0xff) };
            byte[] _addr = destination.Address.GetAddressBytes();

            Array.Copy(_addr, 0, req, 4, 4);
            Array.Copy(_port, 0, req, 8, 2);

            //Debug.PrintPacket(req, "connect request");
            wrt.Write(req);
            resp = new byte[2];
            resp = rdr.ReadBytes(2);
            //Debug.PrintPacket(req, "connect response");

            if (resp[0] != 5) throw new Socks5proxyException(string.Format("proxy verision reponse != 5 [{0:X}]", resp[0]));
            if (resp[1] != 0)
            {
                string error = "";
                switch (resp[1])
                {
                    case 1: error = "general SOCKS server failure"; break;
                    case 2: error = "connection not allowed by ruleset"; break;
                    case 3: error = "Network unreachable"; break;
                    case 4: error = "Host unreachable"; break;
                    case 5: error = "Connection refused"; break;
                    case 6: error = "TTL expired"; break;
                    case 7: error = "Command not supported"; break;
                    case 8: error = "Address type not supported"; break;

                    default: error = "unknown"; break;
                }
                throw new Socks5proxyException(string.Format("proxy connect error [{0:X}][{1}]", resp[1], error));
            }

            rdr.ReadByte();//skip reserved
            byte type = rdr.ReadByte();

            switch (type)
            {
                case 1:
                    resp = rdr.ReadBytes(4);
                    IPAddress bindaddr = new IPAddress(resp);
                    resp = rdr.ReadBytes(2);
                    int bindport = (resp[0] << 8) & 0xff00 + resp[1];
                    //Console.WriteLine(String.Format("addres bind:{0},{1}", bindaddr.Address.ToString(), bindport));

                    return this.tcpClient;

                default: throw new Socks5proxyException(String.Format("unsupported ATYP [{0}]", type));
            }

            //   +----+-----+-------+------+----------+----------+
            //   |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
            //   +----+-----+-------+------+----------+----------+
            //   | 1  |  1  | X'00' |  1   | Variable |    2     |
            //   +----+-----+-------+------+----------+----------+

            //Where:

            //     o  VER    protocol version: X'05'
            //     o  REP    Reply field:
            //        o  X'00' succeeded
            //        o  X'01' general SOCKS server failure
            //        o  X'02' connection not allowed by ruleset
            //        o  X'03' Network unreachable
            //        o  X'04' Host unreachable
            //        o  X'05' Connection refused
            //        o  X'06' TTL expired
            //        o  X'07' Command not supported
            //        o  X'08' Address type not supported
            //        o  X'09' to X'FF' unassigned
            //     o  RSV    RESERVED
            //     o  ATYP   address type of following address

            //        o  IP V4 address: X'01'
            //        o  DOMAINNAME: X'03'
            //        o  IP V6 address: X'04'
            //     o  BND.ADDR       server bound address
            //     o  BND.PORT       server bound port in network octet order


            throw new Exception("stop");

        }


    }


}
namespace test
{
    public class Program
    {
        public static void Main(string[] args)
        {

            Proxy proxy = new Proxy(IPAddress.Loopback, 7771);
            TcpClient client = proxy.ConnectTcpClient("test.thesebas.info", 80, false);
            NetworkStream ns = client.GetStream();
            StreamWriter swr = new StreamWriter(ns);
            swr.AutoFlush = true;
            StreamReader srd = new StreamReader(ns);

            swr.WriteLine("HEAD /addr.php HTTP/1.1");
            swr.WriteLine("Host: test.thesebas.info");
            swr.WriteLine();
            string resp = srd.ReadLine();
            while (resp != "" && resp != null)
            {
                Console.WriteLine(resp);
                resp = srd.ReadLine();
            }
            Console.ReadKey();
        }
    }

}

